From 396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b Mon Sep 17 00:00:00 2001
From: Fuwn <50817549+Fuwn@users.noreply.github.com>
Date: Sat, 24 Jan 2026 13:09:50 +0000
Subject: Initial commit
Created from https://vercel.com/new
---
.../[websiteId]/sessions/SessionActivity.tsx | 94 +++++++++++++++++++++
.../websites/[websiteId]/sessions/SessionData.tsx | 32 +++++++
.../websites/[websiteId]/sessions/SessionInfo.tsx | 85 +++++++++++++++++++
.../websites/[websiteId]/sessions/SessionModal.tsx | 41 +++++++++
.../[websiteId]/sessions/SessionProfile.tsx | 84 +++++++++++++++++++
.../[websiteId]/sessions/SessionProperties.tsx | 97 ++++++++++++++++++++++
.../websites/[websiteId]/sessions/SessionStats.tsx | 21 +++++
.../[websiteId]/sessions/SessionsDataTable.tsx | 15 ++++
.../[websiteId]/sessions/SessionsMetricsBar.tsx | 40 +++++++++
.../websites/[websiteId]/sessions/SessionsPage.tsx | 43 ++++++++++
.../[websiteId]/sessions/SessionsTable.tsx | 58 +++++++++++++
.../(main)/websites/[websiteId]/sessions/page.tsx | 12 +++
12 files changed, 622 insertions(+)
create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionActivity.tsx
create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionData.tsx
create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionInfo.tsx
create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx
create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionProfile.tsx
create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx
create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionStats.tsx
create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx
create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx
create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx
create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx
create mode 100644 src/app/(main)/websites/[websiteId]/sessions/page.tsx
(limited to 'src/app/(main)/websites/[websiteId]/sessions')
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionActivity.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionActivity.tsx
new file mode 100644
index 0000000..cbb2810
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionActivity.tsx
@@ -0,0 +1,94 @@
+import {
+ Button,
+ Column,
+ Dialog,
+ DialogTrigger,
+ Heading,
+ Icon,
+ Popover,
+ Row,
+ StatusLight,
+ Text,
+} from '@umami/react-zen';
+import { isSameDay } from 'date-fns';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useMessages, useMobile, useSessionActivityQuery, useTimezone } from '@/components/hooks';
+import { Eye, FileText } from '@/components/icons';
+import { EventData } from '@/components/metrics/EventData';
+import { Lightning } from '@/components/svg';
+
+export function SessionActivity({
+ websiteId,
+ sessionId,
+ startDate,
+ endDate,
+}: {
+ websiteId: string;
+ sessionId: string;
+ startDate: Date;
+ endDate: Date;
+}) {
+ const { formatMessage, labels } = useMessages();
+ const { formatTimezoneDate } = useTimezone();
+ const { data, isLoading, error } = useSessionActivityQuery(
+ websiteId,
+ sessionId,
+ startDate,
+ endDate,
+ );
+ const { isMobile } = useMobile();
+ let lastDay = null;
+
+ return (
+
+
+ {data?.map(({ eventId, createdAt, urlPath, eventName, visitId, hasData }) => {
+ const showHeader = !lastDay || !isSameDay(new Date(lastDay), new Date(createdAt));
+ lastDay = createdAt;
+
+ return (
+
+ {showHeader && {formatTimezoneDate(createdAt, 'PPPP')}}
+
+
+ {formatTimezoneDate(createdAt, 'pp')}
+
+
+ {eventName ? : }
+
+ {eventName
+ ? formatMessage(labels.triggeredEvent)
+ : formatMessage(labels.viewedPage)}
+
+
+ {eventName || urlPath}
+
+ {hasData > 0 && }
+
+
+
+ );
+ })}
+
+
+ );
+}
+
+const PropertiesButton = props => {
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionData.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionData.tsx
new file mode 100644
index 0000000..7c82c17
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionData.tsx
@@ -0,0 +1,32 @@
+import { Box, Column, Label, Row, Text } from '@umami/react-zen';
+import { Empty } from '@/components/common/Empty';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useSessionDataQuery } from '@/components/hooks';
+import { DATA_TYPES } from '@/lib/constants';
+
+export function SessionData({ websiteId, sessionId }: { websiteId: string; sessionId: string }) {
+ const { data, isLoading, error } = useSessionDataQuery(websiteId, sessionId);
+
+ return (
+
+ {!data?.length && }
+
+ {data?.map(({ dataKey, dataType, stringValue }) => {
+ return (
+
+
+
+ {stringValue}
+
+
+ {DATA_TYPES[dataType]}
+
+
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionInfo.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionInfo.tsx
new file mode 100644
index 0000000..f15e6ee
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionInfo.tsx
@@ -0,0 +1,85 @@
+import { Column, Grid, Icon, Label, Row } from '@umami/react-zen';
+import type { ReactNode } from 'react';
+import { DateDistance } from '@/components/common/DateDistance';
+import { TypeIcon } from '@/components/common/TypeIcon';
+import { useFormat, useLocale, useMessages, useRegionNames } from '@/components/hooks';
+import { Calendar, KeyRound, Landmark, MapPin } from '@/components/icons';
+
+export function SessionInfo({ data }) {
+ const { locale } = useLocale();
+ const { formatMessage, labels } = useMessages();
+ const { formatValue } = useFormat();
+ const { getRegionName } = useRegionNames(locale);
+
+ return (
+
+ }>
+ {data?.distinctId}
+
+
+ }>
+
+
+
+ }>
+
+
+
+ }
+ >
+ {formatValue(data?.country, 'country')}
+
+
+ }>
+ {getRegionName(data?.region)}
+
+
+ }>
+ {data?.city}
+
+
+ }
+ >
+ {formatValue(data?.browser, 'browser')}
+
+
+ }
+ >
+ {formatValue(data?.os, 'os')}
+
+
+ }
+ >
+ {formatValue(data?.device, 'device')}
+
+
+ );
+}
+
+const Info = ({
+ label,
+ icon,
+ children,
+}: {
+ label: string;
+ icon?: ReactNode;
+ children: ReactNode;
+}) => {
+ return (
+
+
+
+ {icon && {icon}}
+ {children || '—'}
+
+
+ );
+};
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx
new file mode 100644
index 0000000..d658038
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx
@@ -0,0 +1,41 @@
+import { Column, Dialog, Modal, type ModalProps } from '@umami/react-zen';
+import { SessionProfile } from '@/app/(main)/websites/[websiteId]/sessions/SessionProfile';
+import { useNavigation } from '@/components/hooks';
+
+export interface SessionModalProps extends ModalProps {
+ websiteId: string;
+}
+
+export function SessionModal({ websiteId, ...props }: SessionModalProps) {
+ const {
+ router,
+ query: { session },
+ updateParams,
+ } = useNavigation();
+ const handleOpenChange = (isOpen: boolean) => {
+ if (!isOpen) {
+ router.push(updateParams({ session: undefined }));
+ }
+ };
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionProfile.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionProfile.tsx
new file mode 100644
index 0000000..6624d43
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionProfile.tsx
@@ -0,0 +1,84 @@
+import {
+ Button,
+ Column,
+ Icon,
+ Row,
+ Tab,
+ TabList,
+ TabPanel,
+ Tabs,
+ TextField,
+} from '@umami/react-zen';
+import { X } from 'lucide-react';
+import { Avatar } from '@/components/common/Avatar';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useMessages, useWebsiteSessionQuery } from '@/components/hooks';
+import { SessionActivity } from './SessionActivity';
+import { SessionData } from './SessionData';
+import { SessionInfo } from './SessionInfo';
+import { SessionStats } from './SessionStats';
+
+export function SessionProfile({
+ websiteId,
+ sessionId,
+ onClose,
+}: {
+ websiteId: string;
+ sessionId: string;
+ onClose?: () => void;
+}) {
+ const { data, isLoading, error } = useWebsiteSessionQuery(websiteId, sessionId);
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+ {data && (
+
+ {onClose && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatMessage(labels.activity)}
+ {formatMessage(labels.properties)}
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx
new file mode 100644
index 0000000..1693d05
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx
@@ -0,0 +1,97 @@
+import { Column, Grid, ListItem, Select } from '@umami/react-zen';
+import { useMemo, useState } from 'react';
+import { PieChart } from '@/components/charts/PieChart';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import {
+ useMessages,
+ useSessionDataPropertiesQuery,
+ useSessionDataValuesQuery,
+} from '@/components/hooks';
+import { ListTable } from '@/components/metrics/ListTable';
+import { CHART_COLORS } from '@/lib/constants';
+
+export function SessionProperties({ websiteId }: { websiteId: string }) {
+ const [propertyName, setPropertyName] = useState('');
+ const { formatMessage, labels } = useMessages();
+ const { data, isLoading, isFetching, error } = useSessionDataPropertiesQuery(websiteId);
+
+ const properties: string[] = data?.map(e => e.propertyName);
+
+ return (
+
+
+ {data && (
+
+
+
+ )}
+ {propertyName && }
+
+
+ );
+}
+
+const SessionValues = ({ websiteId, propertyName }) => {
+ const { data, isLoading, isFetching, error } = useSessionDataValuesQuery(websiteId, propertyName);
+
+ const propertySum = useMemo(() => {
+ return data?.reduce((sum, { total }) => sum + total, 0) ?? 0;
+ }, [data]);
+
+ const chartData = useMemo(() => {
+ if (!propertyName || !data) return null;
+ return {
+ labels: data.map(({ value }) => value),
+ datasets: [
+ {
+ data: data.map(({ total }) => total),
+ backgroundColor: CHART_COLORS,
+ borderWidth: 0,
+ },
+ ],
+ };
+ }, [propertyName, data]);
+
+ const tableData = useMemo(() => {
+ if (!propertyName || !data || propertySum === 0) return [];
+ return data.map(({ value, total }) => ({
+ label: value,
+ count: total,
+ percent: 100 * (total / propertySum),
+ }));
+ }, [propertyName, data, propertySum]);
+
+ return (
+
+ {data && (
+
+
+
+
+ )}
+
+ );
+};
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionStats.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionStats.tsx
new file mode 100644
index 0000000..e25be9a
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionStats.tsx
@@ -0,0 +1,21 @@
+import { useMessages } from '@/components/hooks';
+import { MetricCard } from '@/components/metrics/MetricCard';
+import { MetricsBar } from '@/components/metrics/MetricsBar';
+import { formatShortTime } from '@/lib/format';
+
+export function SessionStats({ data }) {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+
+
+
+
+ `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`}
+ />
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx
new file mode 100644
index 0000000..b1b9f65
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx
@@ -0,0 +1,15 @@
+import { DataGrid } from '@/components/common/DataGrid';
+import { useWebsiteSessionsQuery } from '@/components/hooks';
+import { SessionsTable } from './SessionsTable';
+
+export function SessionsDataTable({ websiteId }: { websiteId?: string; teamId?: string }) {
+ const queryResult = useWebsiteSessionsQuery(websiteId);
+
+ return (
+
+ {({ data }) => {
+ return ;
+ }}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx
new file mode 100644
index 0000000..c8317a2
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx
@@ -0,0 +1,40 @@
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useMessages } from '@/components/hooks';
+import { useWebsiteSessionStatsQuery } from '@/components/hooks/queries/useWebsiteSessionStatsQuery';
+import { MetricCard } from '@/components/metrics/MetricCard';
+import { MetricsBar } from '@/components/metrics/MetricsBar';
+import { formatLongNumber } from '@/lib/format';
+
+export function SessionsMetricsBar({ websiteId }: { websiteId: string }) {
+ const { formatMessage, labels } = useMessages();
+ const { data, isLoading, isFetching, error } = useWebsiteSessionStatsQuery(websiteId);
+
+ return (
+
+ {data && (
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx
new file mode 100644
index 0000000..8e9d2f2
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx
@@ -0,0 +1,43 @@
+'use client';
+import { Column, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
+import { type Key, useState } from 'react';
+import { SessionModal } from '@/app/(main)/websites/[websiteId]/sessions/SessionModal';
+import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
+import { Panel } from '@/components/common/Panel';
+import { useMessages } from '@/components/hooks';
+import { getItem, setItem } from '@/lib/storage';
+import { SessionProperties } from './SessionProperties';
+import { SessionsDataTable } from './SessionsDataTable';
+
+const KEY_NAME = 'umami.sessions.tab';
+
+export function SessionsPage({ websiteId }) {
+ const [tab, setTab] = useState(getItem(KEY_NAME) || 'activity');
+ const { formatMessage, labels } = useMessages();
+
+ const handleSelect = (value: Key) => {
+ setItem(KEY_NAME, value);
+ setTab(value);
+ };
+
+ return (
+
+
+
+
+
+ {formatMessage(labels.activity)}
+ {formatMessage(labels.properties)}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx
new file mode 100644
index 0000000..5d3bb37
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx
@@ -0,0 +1,58 @@
+import { DataColumn, DataTable, type DataTableProps } from '@umami/react-zen';
+import Link from 'next/link';
+import { Avatar } from '@/components/common/Avatar';
+import { DateDistance } from '@/components/common/DateDistance';
+import { TypeIcon } from '@/components/common/TypeIcon';
+import { useFormat, useMessages, useNavigation } from '@/components/hooks';
+
+export function SessionsTable(props: DataTableProps) {
+ const { formatMessage, labels } = useMessages();
+ const { formatValue } = useFormat();
+ const { updateParams } = useNavigation();
+
+ return (
+
+
+ {(row: any) => (
+
+
+
+ )}
+
+
+
+
+ {(row: any) => (
+
+ {formatValue(row.country, 'country')}
+
+ )}
+
+
+
+ {(row: any) => (
+
+ {formatValue(row.browser, 'browser')}
+
+ )}
+
+
+ {(row: any) => (
+
+ {formatValue(row.os, 'os')}
+
+ )}
+
+
+ {(row: any) => (
+
+ {formatValue(row.device, 'device')}
+
+ )}
+
+
+ {(row: any) => }
+
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/page.tsx b/src/app/(main)/websites/[websiteId]/sessions/page.tsx
new file mode 100644
index 0000000..221ab71
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { SessionsPage } from './SessionsPage';
+
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
+ const { websiteId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Sessions',
+};
--
cgit v1.2.3